use std::collections::HashSet;

use ndarray::Array2;
use rand::prelude::ThreadRng;

use crate::tiling::slot_grid::{
    DIRECTION_COUNTERPART_OFFSET_MAPPING,
    EAST_SEEKING_DIRECTIONS,
    NORTH_SEEKING_DIRECTIONS,
    SOUTH_SEEKING_DIRECTIONS,
    WEST_SEEKING_DIRECTIONS,
};
use crate::tiling::tiles::tile_list::TileList;
use crate::tiling::tiles::{ArrayInitializable, Connectors, TileConnection, TileConnectionArray};
use crate::tiling::{
    Direction,
    GridSlot,
    MissingTilePolicy,
    SlotGrid,
    TileSelectionPolicy,
    VacancyPolicy,
};
use crate::Error;

#[cfg(test)]
mod tests;

impl SlotGrid {
    pub(crate) fn new(
        num_rows: usize,
        num_cols: usize,
        tile_list: TileList,
        border_tile_id: Option<usize>,
        missing_tile_policy: MissingTilePolicy,
        tile_selection_policy: TileSelectionPolicy,
        vacancy_policy: VacancyPolicy,
    ) -> Result<Self, Error> {
        if num_rows < 1 || num_cols < 1 {
            return Err(Error::ValueError(format!(
                "num_rows and num_cols must be > 0, got num_rows = {num_rows} and num_cols = \
                 {num_cols}"
            )));
        }
        if let Some(border_tile_id) = border_tile_id {
            if border_tile_id >= tile_list.tiles.len() {
                return Err(Error::ValueError(format!(
                    "Invalid border tile ID: {border_tile_id}"
                )));
            }
        }

        Ok(SlotGrid {
            // Grid contents are shifted by one row and one column in order to avoid negative
            // indices and the use of a signed datatype for indexing, therefore grid has an
            // additional zeroth row and column only containing None values.
            grid: Array2::default((num_rows + 1, num_cols + 1)),
            tile_connections: TileConnectionArray::new(&tile_list.tiles),
            tile_list,
            border_tile_id,
            missing_tile_policy,
            tile_selection_policy,
            vacancy_policy,
            num_rows,
            num_cols,
            current_slot: ((num_rows + 1) / 2, (num_cols + 1) / 2),
            current_predecessor: None,
            last_leaves: Vec::new(),
            exposed_edge_count: 0,
        })
    }

    pub(crate) fn populate_simple(&mut self) -> Result<(), Error> {
        // Instantiate a random number generator
        // in case self.tile_selection_policy == TileSelectionPolicy::Random.
        let mut rng: ThreadRng = rand::rng();

        for row in 1..=self.num_rows {
            for col in 1..=self.num_cols {
                let tile_connection = self.determine_tile_connection(
                    &self.determine_base_tile_connectors(row, col),
                    &[],
                    &HashSet::new(),
                )?;

                // Make a choice of the tiles in tile_connection.tiles, weighted with
                // tile_connection.weights, then insert a GridSlot with the chosen tile.
                self.grid[[row, col]] = Some(GridSlot {
                    tile_id: Some(match self.tile_selection_policy {
                        TileSelectionPolicy::Priority => tile_connection.select_tile_by_priority(),
                        TileSelectionPolicy::Random => {
                            tile_connection.select_tile_randomly(&mut rng)?
                        },
                    }),
                    direction: None,
                    predecessor: None,
                    exposed_edges: vec![],
                    excluded_connectors: HashSet::new(),
                });
            }
        }
        Ok(())
    }

    pub(crate) fn populate(&mut self) -> Result<(), Error> {
        // The main population loop in which a tile is added to the current slot grid
        // each iteration.
        loop {
            self.grid[self.current_slot] = Some(GridSlot::new(self.current_predecessor));

            self.fill_slot()?;

            let done = self.prepare_next_iteration();
            if done {
                // There are no exposed edges left, so there are either no vacancies or they are
                // ignored, the tile grid is done.
                return Ok(());
            }
        }
    }

    fn fill_slot(&mut self) -> Result<(), Error> {
        // The slot contains no tile, so try selecting a fitting tile.
        let (tile_id, inward_connectors) = loop {
            let inward_connectors =
                self.determine_base_tile_connectors(self.current_slot.0, self.current_slot.1);

            // Determine tiles and their weights that might fit into this slot.
            match self.determine_tile_connection(
                &inward_connectors,
                &self.determine_neighbor_associations(),
                &self.grid[self.current_slot]
                    .as_ref()
                    .expect("the current slot should be Some(GridSlot)")
                    .excluded_connectors,
            ) {
                Ok(tile_connection) => {
                    // There are tiles available to choose for this slot.
                    if let Some(tile_id) = self.choose_tile(tile_connection) {
                        break (tile_id, inward_connectors);
                    }
                },
                Err(error) => {
                    match self.missing_tile_policy {
                        MissingTilePolicy::Avoid => {
                            // Perform backtracking, which yields different coordinates, which are
                            // used in the next loop execution.
                            self.backtrack()?
                        },
                        MissingTilePolicy::Exit => return Err(error),
                    }
                },
            }
        };

        // The tile selection was successful for this slot (for now).
        // Take care of the four slots around it and determine the exposed edges.
        let mut exposed_edges = vec![];
        for (side, counterpart, offset) in DIRECTION_COUNTERPART_OFFSET_MAPPING {
            // Make sure to skip edges at the grid border.
            let coordinates = Self::offset_coordinates(self.current_slot, offset);
            if self.coordinates_are_in_borders(coordinates) {
                if inward_connectors.get_connector(side) > 1 {
                    // The inward connector at side is non-blank, so there was an exposed edge.
                    self.grid[coordinates]
                        .as_mut()
                        .expect("the neighboring slot should be Some(GridSlot)")
                        .remove_exposed_edge(counterpart);
                } else if self.tile_list.tiles[tile_id].connectors.get_connector(side) > 1 {
                    // The selected tile has an exposed edge towards side.
                    exposed_edges.push(side);
                }
            }
        }

        let current_slot = self.grid[self.current_slot]
            .as_mut()
            .expect("the current slot should be Some(GridSlot)");
        current_slot.tile_id = Some(tile_id);
        current_slot.exposed_edges = exposed_edges;
        current_slot.direction = current_slot.next_exposed_edge();

        Ok(())
    }

    fn prepare_next_iteration(&mut self) -> bool {
        // self.grid[self.current_slot] is needed as immutable and then mutable
        // reference, therefore two separate borrows occur. More elegant
        // solutions were not found.
        if let Some(current_slot) = self.grid[self.current_slot].as_ref() {
            if current_slot.direction.is_none() && !self.current_slot_has_successors() {
                // There are no exposed edges and no successors, so the current slot is a leaf.
                self.last_leaves.push(self.current_slot);
            }
        }
        if let Some(mut current_slot) = self.grid[self.current_slot].as_mut() {
            // Process the dead-end case.
            while current_slot.direction.is_none() {
                // This slot has no exposed edges, this branch is done (closed) for now.

                if let Some(predecessor) = current_slot.predecessor {
                    // Go back to predecessor
                    self.current_slot = predecessor;
                } else {
                    // No predecessor left, the first slot was reached.
                    self.current_predecessor = None;

                    return true;
                }
                current_slot = self.grid[self.current_slot]
                    .as_mut()
                    .expect("the predecessor should be Some(GridSlot)");

                // In case there is no exposed edge, this branch of slots is done and
                // current_slot.direction will be None.
                current_slot.direction = current_slot.next_exposed_edge();
            }

            self.current_predecessor = Some(self.current_slot);
            // Prepare for the next iteration of the main population loop.
            match current_slot
                .direction
                .expect("direction should be Some(Direction)")
            {
                Direction::North => self.current_slot.0 -= 1,
                Direction::East => self.current_slot.1 += 1,
                Direction::South => self.current_slot.0 += 1,
                Direction::West => self.current_slot.1 -= 1,
            }
        }

        false
    }

    #[allow(clippy::needless_range_loop)] // The i,j-range loop is, in fact, not needless.
    fn determine_neighbor_associations(&self) -> Vec<HashSet<Direction>> {
        match self.vacancy_policy {
            // Determine necessary neighbor associations using a fast heuristic approach.
            VacancyPolicy::Avoid => {
                let mut array: [[bool; 3]; 3] = [[false; 3]; 3];

                for i in 0usize..3 {
                    for j in 0usize..3 {
                        let coordinates =
                            (self.current_slot.0 + i - 1, self.current_slot.1 + j - 1);
                        array[i][j] = !self.coordinates_are_in_borders(coordinates)
                            || self.grid[coordinates].is_some();
                    }
                }

                // The following match expression has hundreds of very similar match arms, so
                // the code is generated using neighbor_associations_match_generation.py
                let neighbor_associations =
                    include!(concat!(env!("OUT_DIR"), "/neighbor_associations_match.rs"));

                if self.exposed_edge_count > 0 && neighbor_associations.len() < 2 {
                    // There are exposed edges and no semi-cavities, so there is no need to
                    // require the candidate slot to expose connectors.
                    vec![]
                } else {
                    neighbor_associations
                }
            },
            // Search the problem space thoroughly.
            VacancyPolicy::AvoidStrict => self.traverse_neighbor_associations(None).0,
            // Ignore cavities in the grid.
            VacancyPolicy::Ignore => vec![],
        }
    }

    fn choose_tile(&mut self, tile_connection: TileConnection) -> Option<usize> {
        // Choose a tile from tile_connection considering the corresponding weights.
        let candidate_tile_id = tile_connection.select_tile_by_priority();

        if self.vacancy_policy == VacancyPolicy::Ignore {
            // As vacancies are to be ignored, it would not matter if vacant neighbors could
            // not be filled, so no checks are needed.
            return Some(candidate_tile_id);
        }

        // Check if there is a possible tile for every vacant neighbor.
        for (side, counterpart, offset) in DIRECTION_COUNTERPART_OFFSET_MAPPING {
            // Only consider cases where the calculated position is in bounds, i.e.,
            // Some(…).
            if let Some(slot) = self.get_neighbor_slot(offset) {
                // Check if the slot is vacant, i.e. None.
                if slot.is_none() {
                    // The candidate tile needs to be taken into consideration when trying to
                    // determine a TileConnection for this neighbor, so the relevant connector has
                    // to be overwritten.
                    let mut connectors = self.determine_base_tile_connectors(
                        (self.current_slot.0 as isize + offset.0) as usize,
                        (self.current_slot.1 as isize + offset.1) as usize,
                    );
                    let candidate_connectors =
                        self.tile_list.tiles[candidate_tile_id].connectors.clone();
                    connectors.set_connector(counterpart, candidate_connectors.get_connector(side));

                    // Check if trying to determine a TileConnection for this neighbor fails.
                    if self
                        .determine_tile_connection(&connectors, &[], &HashSet::new())
                        .is_err()
                    {
                        // The checked neighbor is in bounds, vacant, and there is no solution with
                        // the chosen tile, so exclude the tile.
                        if let Some(current_slot) = self.grid[self.current_slot].as_mut() {
                            // The current slot actually exists.
                            current_slot.add_excluded_connectors(candidate_connectors);
                        }
                        return None;
                    }
                }
            }
        }

        // All vacant neighbors are fine.
        Some(candidate_tile_id)
    }

    fn backtrack(&mut self) -> Result<(), Error> {
        // There are no tiles that fit into this slot, therefore backtracking is
        // necessary. It has to be performed to the point where this slot is
        // reachable from the (then probably different) current slot.
        if let Some(first_predecessor) = self.current_predecessor {
            // This slot is not the root (first) slot, so there is hope for backtracking.
            let initial_slot = self.current_slot;

            // Delete the current slot as there are no tiles that can fit.
            // The slot is expected to already be reset, if applicable, so do not reset.
            self.grid[self.current_slot] = None;

            // Go back to the first predecessor (first hop in backtracking).
            self.current_slot = first_predecessor;

            loop {
                if self.current_slot_has_successors() {
                    // There are successors, which means that there is a subtree of slot nodes
                    // below this slot. To ensure that a proper depth-first search is performed on
                    // this subtree, a jump to the last leaf is necessary.
                    self.current_slot = self
                        .last_leaves
                        .pop()
                        .expect("there should be last leaves when there is a successor");
                }

                if let Some(last_leaf_slot) = &self.grid[self.current_slot] {
                    self.current_predecessor = last_leaf_slot.predecessor
                } else {
                    return Err(Error::ValueError(format!(
                        "current slot (coordinates {:?}) does not exist",
                        self.current_slot
                    )));
                }

                // Determine whether initial_slot is reachable from the current slot.
                if self.traverse_neighbor_associations(Some(initial_slot)).1 {
                    // initial_slot is reachable from the current slot.
                    break;
                }

                // Delete the current slot.
                self.reset_current_slot();
                self.grid[self.current_slot] = None;

                // Go back to the predecessor.
                self.current_slot = self
                    .current_predecessor
                    .expect("the current slot should have some predecessor");
                self.current_predecessor = self.grid[self.current_slot]
                    .as_ref()
                    .expect("the current slot should exist")
                    .predecessor;
            }
            // Only reset the current slot without deleting it, its memory of excluded
            // connectors is needed.
            self.reset_current_slot();
            Ok(())
        } else {
            // There is no predecessor to go to, the first slot was reached.
            // Backtracking isn’t possible anymore.
            Err(Error::TileNotFoundException(
                "no more tiles left to try, backtracking failed.".to_string(),
            ))
        }
    }

    fn traverse_neighbor_associations(
        &self,
        wanted_slot: Option<(usize, usize)>,
    ) -> (Vec<HashSet<Direction>>, bool) {
        // This holds the mapping of each of the current tile’s sides
        // (Direction) with an exposed edge to the coordinates of the
        // respective neighbor.
        let mut vacant_neighbors: Vec<(Direction, (usize, usize))> = vec![];
        let mut neighbor_associations: Vec<HashSet<Direction>> = vec![];

        for (direction, _, offset) in DIRECTION_COUNTERPART_OFFSET_MAPPING {
            let neighbor = Self::offset_coordinates(self.current_slot, offset);
            if self.coordinates_are_in_borders(neighbor) {
                if let Some(slot) = &self.grid[neighbor] {
                    if slot.tile_id.is_none() {
                        vacant_neighbors.push((direction, neighbor));
                    }
                } else {
                    vacant_neighbors.push((direction, neighbor));
                }
            }
        }

        // Iterate over the vacant neighbors. Every time vacant_neighbors is popped, a
        // new neighbor association is entered. Use the first neighbors direction in
        // relation to self.current_slot and its coordinates as a starting point
        // (cursor).
        while let Some((first_neighbor_direction, mut cursor)) = vacant_neighbors.pop() {
            let mut neighbor_association = HashSet::from([first_neighbor_direction]);

            // Start with assuming there are no connectors facing into this semi-cavity.
            let mut exposed_edge_found = false;

            // Iterate through the semi-cavity of the current neighbor in self.grid in
            // search of other vacant_neighbors.
            // Use the first neighbors direction for determining which direction to go next.
            let mut current_direction = first_neighbor_direction;
            loop {
                let wanted_slot_reached;
                (
                    cursor,
                    current_direction,
                    exposed_edge_found,
                    wanted_slot_reached,
                ) = self.find_next_slot_in_neighbor_association(
                    first_neighbor_direction,
                    cursor,
                    current_direction,
                    exposed_edge_found,
                    wanted_slot,
                );

                if wanted_slot_reached {
                    return (vec![], true);
                }

                if cursor == self.current_slot {
                    // The first neighbor was reached, no more neighbors left to find.
                    if !exposed_edge_found {
                        // There was no open connection found at the border of the semi-cavity, so
                        // there has to be a connection in this neighbor_association.
                        neighbor_associations.push(neighbor_association);
                    }
                    break;
                } else {
                    // neighbor_direction is in relation to self.current_slot.
                    vacant_neighbors.retain(|&(neighbor_direction, neighbor)| {
                        {
                            if neighbor == cursor {
                                // A different neighbor was reached.
                                neighbor_association.insert(neighbor_direction);
                                false
                            } else {
                                true
                            }
                        }
                    });
                }
            }
        }

        (neighbor_associations, false)
    }

    fn find_next_slot_in_neighbor_association(
        &self,
        first_neighbor_direction: Direction,
        cursor: (usize, usize),
        direction: Direction,
        mut exposed_edge_found: bool,
        wanted_slot: Option<(usize, usize)>,
    ) -> ((usize, usize), Direction, bool, bool) {
        // Check if the wanted slot was reached.
        if let Some(wanted_slot) = wanted_slot {
            if cursor == wanted_slot {
                // The new cursor coordinates, direction and exposed_edge_found are deliberately
                // hard-coded, because if there is a wanted slot then only the last boolean,
                // which signifies whether the wanted slot was found, is taken into
                // consideration by the caller.
                return ((0, 0), Direction::North, false, true);
            }
        }

        let seeking_directions = match direction {
            Direction::North => NORTH_SEEKING_DIRECTIONS,
            Direction::East => EAST_SEEKING_DIRECTIONS,
            Direction::South => SOUTH_SEEKING_DIRECTIONS,
            Direction::West => WEST_SEEKING_DIRECTIONS,
        };
        for (suggested_direction, offset) in seeking_directions {
            let next_cursor = Self::offset_coordinates(cursor, offset);
            if !self.coordinates_are_in_borders(next_cursor) {
                continue;
            }

            if next_cursor == self.current_slot {
                // In case the cursor is not on the first neighbor, the traversal needs to
                // continue.
                if suggested_direction
                    == match first_neighbor_direction {
                        Direction::North => Direction::South,
                        Direction::East => Direction::West,
                        Direction::South => Direction::North,
                        Direction::West => Direction::East,
                    }
                {
                    // The first neighbor was reached and the suggested direction is towards
                    // self.current_slot, so the traversal is finished.
                    // Point the cursor to the current slot to signify being done.
                    return (
                        self.current_slot,
                        suggested_direction,
                        exposed_edge_found,
                        false,
                    );
                }

                // Avoid going to the current slot; circumnavigate it by continuing to the next
                // suggested direction in order to reach the first neighbor to ensure the entire
                // border of this semi-cavity is traversed.
                continue;
            }

            // The unstable “let_chains” feature in Edition 2024 would allow for a simpler
            // flow:
            // if let Some(slot) = &self.grid[next_cursor]  && slot.tile_id.is_none()
            if let Some(slot) = &self.grid[next_cursor] {
                if let Some(tile_id) = slot.tile_id {
                    // There is a tile in the checked location, check if the tile has an exposed
                    // edge (i.e., connector) facing where the cursor came from.
                    if !exposed_edge_found
                        && self.tile_list.tiles[tile_id].connectors.get_connector(
                            match suggested_direction {
                                Direction::North => Direction::South,
                                Direction::East => Direction::West,
                                Direction::South => Direction::North,
                                Direction::West => Direction::East,
                            },
                        ) != 1
                    {
                        // There is an exposed edge at the border of the semi-cavity, so there is no
                        // necessity for this neighbor_association.
                        exposed_edge_found = true;
                    }
                } else {
                    // There is no tile in the checked location, so go there for the next iteration.
                    return (next_cursor, suggested_direction, exposed_edge_found, false);
                }
            } else {
                // There is no slot in the checked location, so go there for the next iteration.
                return (next_cursor, suggested_direction, exposed_edge_found, false);
            }
        }
        panic!("neither vacant nor wanted nor current slot found");
    }

    fn determine_tile_connection(
        &self,
        connectors: &Connectors,
        neighbor_associations: &[HashSet<Direction>],
        excluded_connectors: &HashSet<Connectors>,
    ) -> Result<TileConnection, Error> {
        fn format_connector_name(connector: &str) -> String {
            if connector == "🃟" || connector == "None" {
                connector.to_string()
            } else {
                format!("“{connector}”")
            }
        }

        // Select the proper tile connection.
        let tile_connection = &self.tile_connections[[
            connectors.north,
            connectors.east,
            connectors.south,
            connectors.west,
        ]];

        // Exclude tiles with connectors that are in excluded_connectors.
        let tiles = tile_connection
            .tiles
            .iter()
            .map(|tile_id| &self.tile_list.tiles[*tile_id])
            .filter(|tile| !excluded_connectors.contains(&tile.connectors));

        // Exclude tiles without certain connectors according to neighbor_associations.
        // For a tile to be included, it needs to have at least one non-blank connector
        // (ID ≠ 1) in each neighbor association.
        //
        // Example:
        // ```rust
        // let neighbor_associations = vec![
        //     // first neighbor association
        //     HashSet::from([Direction::North, Direction::West]),
        //     HashSet::from([Direction::South]),
        // ];
        // let tile_1 = Tile {
        //     connectors: Connectors { north: 1, south: 3, west: 2, east: 1 },
        //     ⋮
        // };
        // let tile_2 = Tile {
        //     connectors: Connectors { north: 1, south: 3, west: 1, east: 1 },
        //     ⋮
        // };
        // ```
        //
        // tile_1 is to be included because it has a non-blank connector in the first
        // neighbor association (west: 2) and in the second one (south: 3). tile_2
        // however exhibits a blank connector in both directions in the first neighbor
        // association (north: 1, west: 1), so it does not satisfy the first neighbor
        // association and therefore is to be excluded from the tile selection.

        // Filter tiles to only include tiles that satisfy the neighbor association
        // condition.
        let tiles = tiles.filter(
            // The condition for every neighbor association needs to be fulfilled.
            |tile| {
                neighbor_associations.iter().all(
                    // The condition for at least one of the sides needs to be satisfied.
                    |neighbor_association| {
                        neighbor_association.iter().any(
                            // The condition for each connector is, that it needs to be
                            // non-blank (ID ≠ 1).
                            |&side| tile.connectors.get_connector(side) != 1,
                        )
                    },
                )
            },
        );

        let (tiles, weights) = tiles.map(|tile| (tile.id, tile.weight)).unzip();
        let tile_connection = TileConnection { tiles, weights };

        if tile_connection.tiles.is_empty() {
            Err(Error::TileNotFoundException(format!(
                "no tiles available with Connectors {{ north: {}, east: {}, south: {}, west: {} }}",
                format_connector_name(&self.tile_list.vertical_connector_names[connectors.north]),
                format_connector_name(&self.tile_list.horizontal_connector_names[connectors.east]),
                format_connector_name(&self.tile_list.vertical_connector_names[connectors.south]),
                format_connector_name(&self.tile_list.horizontal_connector_names[connectors.west]),
            )))
        } else {
            Ok(tile_connection)
        }
    }

    fn reset_current_slot(&mut self) {
        // None acts as a default value in case there is no tile in this slot.
        let mut connectors: Option<Connectors> = None;

        if let Some(slot) = &mut self.grid[self.current_slot] {
            if let Some(tile_id) = slot.tile_id {
                let tile_connectors = self.tile_list.tiles[tile_id].connectors.clone();
                // Add the tile’s connectors to this slot’s excluded connectors to prevent the
                // tile from being chosen again for this slot.
                connectors = Some(tile_connectors.clone());
                slot.add_excluded_connectors(tile_connectors);
            }
            slot.tile_id = None;
            slot.direction = None;
            slot.clear_exposed_edges();
        }

        // Re-add exposed edges to neighboring slots.
        if let Some(connectors) = connectors {
            // Iterate over all edges of the slot.
            for (side, counterpart, offset) in DIRECTION_COUNTERPART_OFFSET_MAPPING {
                // Only consider edges where the connector is non-blank.
                if connectors.get_connector(side) != 1 {
                    if let Some(Some(slot)) = self.get_neighbor_slot(offset) {
                        slot.add_exposed_edge(counterpart);
                    }
                }
            }
        }
    }

    /// Retrieves an optional mutable reference to a `GridSlot` offset from the
    /// current slot.
    ///
    /// This method calculates the position by applying the given offset to the
    /// specified row and column. If the resulting position is within the
    /// bounds of the grid, it returns a mutable reference to the `GridSlot`
    /// wrapped in an `Option`. If the position is out of bounds, it returns
    /// `None`.
    ///
    /// # Parameters
    ///
    /// - `row`: The row index of the original position.
    /// - `col`: The column index of the original position.
    /// - `offset`: A tuple containing the row and column offset to be applied.
    ///
    /// # Returns
    ///
    /// An `Option` containing another `Option` which holds a mutable reference
    /// to the `GridSlot` if the calculated position is within bounds. If
    /// the calculated position is out of bounds, returns `None`.
    ///
    /// The three possible variants are:
    /// - `None`: The calculated position is out of bounds.
    /// - `Some(None)`: The calculated position is within bounds but there is no
    ///   slot.
    /// - `Some(Some(&mut GridSlot))`: The calculated position is within bounds
    ///   and there is a slot.
    fn get_neighbor_slot(&mut self, offset: (isize, isize)) -> Option<Option<&mut GridSlot>> {
        let coordinates = Self::offset_coordinates(self.current_slot, offset);
        if self.coordinates_are_in_borders(coordinates) {
            Some(self.grid[coordinates].as_mut())
        } else {
            None
        }
    }

    fn current_slot_has_successors(&self) -> bool {
        DIRECTION_COUNTERPART_OFFSET_MAPPING
            .iter()
            .any(|(.., offset)| self.check_for_successor(offset))
    }

    fn check_for_successor(&self, offset: &(isize, isize)) -> bool {
        let candidate = Self::offset_coordinates(self.current_slot, *offset);

        if !self.coordinates_are_in_borders(candidate) {
            return false;
        }

        match self.grid[candidate].as_ref() {
            None => false,
            Some(slot) => slot.predecessor == Some(self.current_slot),
        }
    }

    /// Return the Connectors with the minimum of conditions based on the four
    /// tiles around the tile at the given coordinates. For sides where
    /// there is no tile, the value `0` is used to represent the special
    /// wildcard connector.
    fn determine_base_tile_connectors(&self, row: usize, col: usize) -> Connectors {
        Connectors {
            north: self.get_tile_connector((row - 1, col), Direction::South),
            east: self.get_tile_connector((row, col + 1), Direction::West),
            south: self.get_tile_connector((row + 1, col), Direction::North),
            west: self.get_tile_connector((row, col - 1), Direction::East),
        }
    }

    /// Return the connector at the specified side of the tile at the given
    /// coordinates.
    fn get_tile_connector(&self, coordinates: (usize, usize), side: Direction) -> usize {
        let tile_id = if self.coordinates_are_in_borders(coordinates) {
            // row and col are in bounds
            match &self.grid[coordinates] {
                None => None,
                Some(slot) => slot.tile_id,
            }
        } else {
            self.border_tile_id
        };

        match tile_id {
            None => 0,
            Some(tile_id) => {
                let tile = &self.tile_list.tiles[tile_id];
                tile.connectors.get_connector(side)
            },
        }
    }

    fn coordinates_are_in_borders(&self, coordinates: (usize, usize)) -> bool {
        0 < coordinates.0
            && coordinates.0 < self.grid.dim().0
            && 0 < coordinates.1
            && coordinates.1 < self.grid.dim().1
    }

    fn offset_coordinates(coordinates: (usize, usize), offset: (isize, isize)) -> (usize, usize) {
        (
            (coordinates.0 as isize + offset.0) as usize,
            (coordinates.1 as isize + offset.1) as usize,
        )
    }
}
